<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>
<script>

const template = `<div><p>Vue</p><p>Template</p></div>`;

const State = {
  initial: 1,
  tagOpen: 2,
  tagName: 3,
  text: 4,
  tagEnd: 5,
  tagEndName: 6,
};

function isAlpha(char) {
  return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}

function tokenize(str) {
  let currentState = State.initial;
  const chars = [];
  const tokens = [];
  while (str) {
    const char = str[0];
    switch (currentState) {
      case State.initial:
        if (char === "<") {
          currentState = State.tagOpen;
          str = str.slice(1);
        } else if (isAlpha(char)) {
          currentState = State.text;
          chars.push(char);
          str = str.slice(1);
        }
        break;
      case State.tagOpen:
        if (isAlpha(char)) {
          currentState = State.tagName;
          chars.push(char);
          str = str.slice(1);
        } else if (char === "/") {
          currentState = State.tagEnd;
          str = str.slice(1);
        }
        break;
      case State.tagName:
        if (isAlpha(char)) {
          chars.push(char);
          str = str.slice(1);
        } else if (char === ">") {
          currentState = State.initial;
          tokens.push({
            type: "tag",
            name: chars.join(""),
          });
          chars.length = 0;
          str = str.slice(1);
        }
        break;
      case State.text:
        if (isAlpha(char)) {
          chars.push(char);
          str = str.slice(1);
        } else if (char === "<") {
          currentState = State.tagOpen;
          tokens.push({
            type: "text",
            content: chars.join(""),
          });
          chars.length = 0;
          str = str.slice(1);
        }
        break;
      case State.tagEnd:
        if (isAlpha(char)) {
          currentState = State.tagEndName;
          chars.push(char);
          str = str.slice(1);
        }
        break;
      case State.tagEndName:
        if (isAlpha(char)) {
          chars.push(char);
          str = str.slice(1);
        } else if (char === ">") {
          currentState = State.initial;
          tokens.push({
            type: "tagEnd",
            name: chars.join(""),
          });
          chars.length = 0;
          str = str.slice(1);
        }
        break;
    }
  }

  return tokens;
}

function parse(str) {
  const tokens = tokenize(str);

  const root = {
    type: "Root",
    children: [],
  };
  const elementStack = [root];

  while (tokens.length) {
    const parent = elementStack[elementStack.length - 1];
    const t = tokens[0];
    switch (t.type) {
      case "tag":
        const elementNode = {
          type: "Element",
          tag: t.name,
          children: [],
        };
        parent.children.push(elementNode);
        elementStack.push(elementNode);
        break;
      case "text":
        const textNode = {
          type: "Text",
          content: t.content,
        };
        parent.children.push(textNode);
        break;
      case "tagEnd":
        elementStack.pop();
        break;
    }
    tokens.shift();
  }

  return root;
}

function dump(node, indent = 0){
  const type = node.type;

  const desc = 
    node.type === "Root" 
    ? "" 
    : node.type === "Element" 
    ? node.tag
    : node.content;

  console.log(`${"-".repeat(indent)}${type}: ${desc}`);

  if(node.children){
    node.children.forEach(child => dump(child, indent + 3));
  }
}

function transformRoot(node) {
  return () => {
    // 如果不是root节点，直接返回
    if (node.type !== "Root") {
      return;
    }

    // node.children[0]是根节点的第一个子节点
    // 不考虑多个根节点的情况
    const vnodeJSAST = node.children[0].jsNode;

    // 创建render函数的声明语句节点
    // 将vnodeJSAST作为render函数的返回值
    node.jsNode = {
      type: "FunctionDecl",
      id: { type: "Identifier", name: "render" },
      params: [],
      body: [
        {
          type: "ReturnStatement",
          return: vnodeJSAST,
        },
      ],
    };
  };
}

function transformElement(node, context){
  return () => {
    if (node.type !== "Element") {
      return;
    }

    // 1.创建h函数调用语句
    const callExp = createCallExpression("h", [
      createStringLiteral(node.tag),
    ]);

    // 2.处理h函数调用参数
    node.children.length === 1
      // 如果只有一个子节点，直接传入子节点的jsNode作为参数
      ? callExp.arguments.push(node.children[0].jsNode)
      // 如果有多个子节点，创建一个ArrayExpression节点作为参数
      : callExp.arguments.push(
          createArrayExpression(node.children.map((c) => c.jsNode))
        );

    // 3.将当前标签节点对应的JSAST添加到jsNode属性上
    node.jsNode = callExp;
  };
}

function transformText(node, context){
  if (node.type !== "Text") {
    return;
  }

  node.jsNode = createStringLiteral(node.content);
}

function traverseNode(ast, context){
  context.currentNode = ast;

  const exitFns = [];

  const transforms = context.nodeTransforms;

  for(let i=0; i<transforms.length; i++){
    const onExit = transforms[i](context.currentNode, context);

    if(onExit){
      exitFns.push(onExit);
    }

    if(!context.currentNode){
      return;
    }
  }

  const children = context.currentNode.children;

  if(children){
    for(let i=0; i<children.length; i++){
      // 递归调用之前，将当前节点设置为父节点
      context.parent = context.currentNode;
      context.childIndex = i;
      const child = children[i];
      traverseNode(child, context);
    }
  }

  let i = exitFns.length;
  while(i--){
    exitFns[i]();
  }
}

function transform(ast){

  const context = {
    // 正在转换的节点
    currentNode: null,
    // 当前正在转换节点的父节点
    parent: null,
    // 当前节点在父节点children中的位置索引
    childIndex: 0,
    replaceNode(node){
      context.parent.children[context.childIndex] = node;

      // 由于直接做了替换，所以需要更新当前节点
      context.currentNode = node;
    },
    removeNode(){
      if(context.parent){
        // 从父节点children中删除当前节点
        context.parent.children.splice(context.childIndex, 1);
        // 当前节点指向null
        context.currentNode = null;
      }
    },
    nodeTransforms:[
      transformRoot,
      transformElement,
      transformText
    ]
  }


  traverseNode(ast, context);
  dump(ast);
}

function createStringLiteral(value) {
  return {
    type: "StringLiteral",
    value,
  };
}

function createIdentifier(name) {
  return {
    type: "Identifier",
    name,
  };
}

function createArrayExpression(elements) {
  return {
    type: "ArrayExpression",
    elements,
  };
}

function createCallExpression(callee, arguments) {
  return {
    type: "CallExpression",
    callee: createIdentifier(callee),
    arguments,
  };
}

const ast = parse(template);
transform(ast);
console.log(ast.jsNode);
</script>